winbrew_engines\windows\exe/
switches.rs1use anyhow::{Result, bail};
2use std::path::Path;
3
4use crate::models::catalog::package::CatalogInstaller;
5use crate::models::install::installer::InstallerType;
6
7pub(super) fn build_install_args(
8 installer: &CatalogInstaller,
9 install_dir: &Path,
10 package_name: &str,
11) -> Result<Vec<String>> {
12 let mut args = installer
13 .installer_switches
14 .as_deref()
15 .map(split_switches)
16 .transpose()?
17 .unwrap_or_default();
18
19 match installer.kind {
20 InstallerType::Exe => {
21 if args.is_empty() {
22 bail!(
23 "missing installer switches for generic exe installer '{}'",
24 package_name
25 );
26 }
27 }
28 InstallerType::Inno => {
29 push_flag_if_missing(&mut args, "/VERYSILENT");
30 push_flag_if_missing(&mut args, "/SUPPRESSMSGBOXES");
31 push_flag_if_missing(&mut args, "/NORESTART");
32 push_flag_if_missing(&mut args, "/SP-");
33
34 if !has_arg_prefix(&args, "/dir=") {
35 args.push(format!(r"/DIR={}", install_dir.display()));
36 }
37 }
38 InstallerType::Nullsoft => {
39 push_flag_if_missing(&mut args, "/S");
40
41 if !has_arg_prefix(&args, "/d=") {
42 args.push(format!(r"/D={}", install_dir.display()));
43 }
44 }
45 InstallerType::Burn => {
46 push_flag_if_missing(&mut args, "/quiet");
47 push_flag_if_missing(&mut args, "/norestart");
48 }
49 _ => {
50 bail!(
51 "native exe backend cannot handle installer kind '{}'",
52 installer.kind.as_str()
53 )
54 }
55 }
56
57 Ok(args)
58}
59
60pub(super) fn split_switches(raw: &str) -> Result<Vec<String>> {
61 let mut args = Vec::new();
62 let mut current = String::new();
63 let mut quote: Option<char> = None;
64
65 for ch in raw.chars() {
66 match ch {
67 '"' | '\'' => match quote {
68 Some(active) if active == ch => {
69 quote = None;
70 }
71 Some(_) => current.push(ch),
72 None => quote = Some(ch),
73 },
74 ch if ch.is_whitespace() && quote.is_none() => {
75 if !current.is_empty() {
76 args.push(std::mem::take(&mut current));
77 }
78 }
79 ch => current.push(ch),
80 }
81 }
82
83 if quote.is_some() {
84 bail!("unterminated quoted installer switches: {raw}");
85 }
86
87 if !current.is_empty() {
88 args.push(current);
89 }
90
91 validate_unique_switches(&args, raw)?;
92
93 Ok(args)
94}
95
96fn validate_unique_switches(args: &[String], raw: &str) -> Result<()> {
97 use std::collections::HashSet;
98
99 let mut seen = HashSet::new();
100
101 for arg in args {
102 let signature = switch_signature(arg);
103
104 if !seen.insert(signature) {
105 bail!("duplicate installer switch detected: {arg} in {raw}");
106 }
107 }
108
109 Ok(())
110}
111
112fn switch_signature(arg: &str) -> String {
113 let trimmed = arg.trim();
114
115 match trimmed.split_once('=') {
116 Some((left, _)) => format!("{}=", left.to_ascii_lowercase()),
117 None => trimmed.to_ascii_lowercase(),
118 }
119}
120
121fn push_flag_if_missing(args: &mut Vec<String>, flag: &str) {
122 if !args.iter().any(|arg| arg.eq_ignore_ascii_case(flag)) {
123 args.push(flag.to_string());
124 }
125}
126
127pub(super) fn has_arg_prefix(args: &[String], prefix: &str) -> bool {
128 args.iter()
129 .any(|arg| arg.to_ascii_lowercase().starts_with(prefix))
130}